/**
 * Software License, Version 1.0 Copyright 2003 The Trustees of Indiana
 * University. All rights reserved. Redistribution and use in source and binary
 * forms, with or without modification, are permitted provided that the
 * following conditions are met: 1) All redistributions of source code must
 * retain the above copyright notice, the list of authors in the original source
 * code, this list of conditions and the disclaimer listed in this license; 2)
 * All redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the disclaimer listed in this license in the
 * documentation and/or other materials provided with the distribution; 3) Any
 * documentation included with all redistributions must include the following
 * acknowledgement: "This product includes software developed by the Community
 * Grids Lab. For further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/." Alternatively, this acknowledgement may
 * appear in the software itself, and wherever such third-party acknowledgments
 * normally appear. 4) The name Indiana University or Community Grids Lab or
 * NaradaBrokering, shall not be used to endorse or promote products derived
 * from this software without prior written permission from Indiana University.
 * For written permission, please contact the Advanced Research and Technology
 * Institute ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5)
 * Products derived from this software may not be called NaradaBrokering, nor
 * may Indiana University or Community Grids Lab or NaradaBrokering appear in
 * their name, without prior written permission of ARTI. Indiana University
 * provides no reassurances that the source code provided does not infringe the
 * patent or any other intellectual property rights of any other entity. Indiana
 * University disclaims any liability to any recipient for claims brought by any
 * other entity based on infringement of intellectual property rights or
 * otherwise. LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH
 * NO WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY
 * GIVES NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
/**
 * Software License, Version 1.0 Copyright 2003 The Trustees of Indiana
 * University. All rights reserved. Redistribution and use in source and binary
 * forms, with or without modification, are permitted provided that the
 * following conditions are met: 1) All redistributions of source code must
 * retain the above copyright notice, the list of authors in the original source
 * code, this list of conditions and the disclaimer listed in this license; 2)
 * All redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the disclaimer listed in this license in the
 * documentation and/or other materials provided with the distribution; 3) Any
 * documentation included with all redistributions must include the following
 * acknowledgement: "This product includes software developed by the Community
 * Grids Lab. For further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/." Alternatively, this acknowledgement may
 * appear in the software itself, and wherever such third-party acknowledgments
 * normally appear. 4) The name Indiana University or Community Grids Lab or
 * NaradaBrokering, shall not be used to endorse or promote products derived
 * from this software without prior written permission from Indiana University.
 * For written permission, please contact the Advanced Research and Technology
 * Institute ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5)
 * Products derived from this software may not be called NaradaBrokering, nor
 * may Indiana University or Community Grids Lab or NaradaBrokering appear in
 * their name, without prior written permission of ARTI. Indiana University
 * provides no reassurances that the source code provided does not infringe the
 * patent or any other intellectual property rights of any other entity. Indiana
 * University disclaims any liability to any recipient for claims brought by any
 * other entity based on infringement of intellectual property rights or
 * otherwise. LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH
 * NO WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY
 * GIVES NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
package cgl.narada.discovery.topics;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Properties;
import java.util.Random;
import java.util.Vector;

import org.apache.log4j.Logger;

import cgl.narada.discovery.topics.messages.EncryptedMessagePayload;
import cgl.narada.discovery.topics.messages.SignedTopicAdvertisement;
import cgl.narada.discovery.topics.messages.TopicDiscoveryRequest;
import cgl.narada.discovery.topics.messages.TopicDiscoveryResponse;
import cgl.narada.event.NBEvent;
import cgl.narada.event.TemplateProfileAndSynopsisTypes;
import cgl.narada.matching.Profile;
import cgl.narada.service.ServiceException;
import cgl.narada.service.ServiceProperties;
import cgl.narada.service.ServicePropertiesFactory;
import cgl.narada.service.client.ClientService;
import cgl.narada.service.client.EventConsumer;
import cgl.narada.service.client.EventProducer;
import cgl.narada.service.client.NBEventListener;
import cgl.narada.service.client.SessionService;
import cgl.narada.service.security.securityprovider.CertificateManager;
import cgl.narada.service.security.securityprovider.CertificateUtil;
import cgl.narada.service.timer.ClockI;
import cgl.narada.service.timer.HRClock;
import cgl.narada.util.SystemInit;
import cgl.narada.util.SystemNotInitializedException;

/**
 * Topic Discovery Client can be used to discover topic UUIDs by querying TDN
 * <br>
 * Created on Apr 8, 2005
 * 
 * @author Harshawardhan Gadgil (hgadgil@grids.ucs.indiana.edu)
 */

public class TopicDiscoveryClient implements NBEventListener {

   static Logger log = Logger.getLogger("TopicDiscoveryClient");

   // Should we start benchmarking... NEEDS recompilation if YES !
   private static final boolean BENCHMARK = false;

   /** Credentials of this Entity */
   private Certificate Credentials;
   private PrivateKey privateKey;

   // Misc
   private ClientService clientService;
   private EventProducer producer;
   private EventConsumer consumer;
   private Profile profile_TOPIC_DISCO_RESPONSE;

   private Vector responses;
   private boolean timedOut;

   private ServiceProperties serviceProperties;
   private String configFilePath;

   private int REMAINING_RESPONSES;

   private class Synclock {

      boolean isLocked;

      public synchronized void lock(long timeout) {
         isLocked = true;
         try {
            wait(timeout);
         } catch (InterruptedException e) {
         }
         timedOut = isLocked;
      }

      public synchronized void unlock() {
         isLocked = false;
         notifyAll();
      }
   }

   private Synclock sync = new Synclock();

   // BECHMARK
   private static FileOutputStream fout = null;
   private ClockI hrClock = null;

   /**
    * Creates a TopicDiscoveryClient for discovering topics<br>
    * Assumes that the host/port/protocol of the broker have been previously
    * determined (using for e.g. the broker discovery mechanisms)
    * 
    * @param clientEntityId
    *           clientId to use
    * @param configFile
    *           Path to ServiceConfiguration.txt file
    * @param _creds
    *           Client certificate
    * @param _priv
    *           Client's private key
    * @param host
    *           The host where a broker is running
    * @param port
    *           The port to which to connect to
    * @param protocol
    *           Protocol to use
    */
   public TopicDiscoveryClient(int clientEntityId, String configFile,
            Certificate _creds, PrivateKey _priv, String host, String port,
            String protocol) {

      configFilePath = configFile;

      ServicePropertiesFactory servicePropertiesFactory = ServicePropertiesFactory
               .getInstance();
      serviceProperties = servicePropertiesFactory
               .getServiceProperties(clientEntityId);

      Credentials = _creds;
      privateKey = _priv;

      responses = new Vector();

      init(host, port, protocol);

      // Create the clock
      String nbHome = null;
      try {
         nbHome = SystemInit.getInitData(SystemInit.SYS_NB_HOME);
      } catch (SystemNotInitializedException e) {
         log.error("", e);
      }

      hrClock = HRClock.getClock(nbHome);
   }

   private void init(String brokerHost, String brokerPort,
            String brokerConnProtocol) {
      Properties prop = new Properties();

      // IMP: Might conflict w/ other ppl's entity IDs Maintain constant
      // for now
      int entityId = (new Random()).nextInt();

      prop.put("hostname", brokerHost);
      prop.put("portnum", brokerPort);

      log.info("Connecting using " + brokerConnProtocol + " @ " + brokerHost
               + ":" + brokerPort + " EntityID[" + entityId + "]");

      try {
         clientService = SessionService.getClientService(entityId,
                  configFilePath);
         clientService.initializeBrokerCommunications(prop, brokerConnProtocol);
         consumer = clientService.createEventConsumer(this);

         // Subscribe to TOPIC_DISCOVERY_RESPONSE
         profile_TOPIC_DISCO_RESPONSE = clientService.createProfile(
                  TemplateProfileAndSynopsisTypes.STRING,
                  Topics.TOPIC_DISCOVERY_RESPONSE);
         consumer.subscribeTo(profile_TOPIC_DISCO_RESPONSE);

         // Create a producer
         producer = clientService.createEventProducer();
         producer.setSuppressRedistributionToSource(true);
      } catch (ServiceException e) {
         log.error("", e);
      }
   }

   /**
    * Utility function to discover a topic. This function waits until a topic
    * discovery response was recieved or a timeout occurred
    * 
    * @param queryType -
    *           Type of query (String, INT, TaG-value etc...
    * @param query -
    *           The query for discovering topics
    * @param TDN_ID -
    *           IF NULL, broadcast the query on TOPIC_DISCOVERY topic, else can
    *           specify a specific TDN to target this query
    * @return An array of TopicAdvertisement objects if TDNs could match the
    *         request, Empty array otherwise
    */
   public SignedTopicAdvertisement[] discover(int queryType, String query,
            String TDN_ID, int maxResponses) {

      // Set the maximum responses
      REMAINING_RESPONSES = maxResponses;

      // Clear previous responses
      responses.clear();
      timedOut = false;

      // Send the request
      TopicDiscoveryRequest topicDiscoReq = new TopicDiscoveryRequest();
      topicDiscoReq.setSearchType(queryType);
      topicDiscoReq.setQuery(query);
      topicDiscoReq.setClientCredentials(Credentials);

      if (TDN_ID == null)
         send(Topics.TOPIC_DISCOVERY_REQUEST, topicDiscoReq.getBytes());
      else
         send(Topics.TDN_TOPIC_BASE + "/" + TDN_ID, topicDiscoReq.getBytes());

      // Wait for timeout !
      // TODO: OR MAX_Responses whichever is earlier ...
      sync.lock(5000);

      if (timedOut)
         log.warn("Specified number of responses not recieved until TIMEOUT !");

      if (responses.size() == 0)
         return new SignedTopicAdvertisement[0];
      else
         return (SignedTopicAdvertisement[]) responses
                  .toArray(new SignedTopicAdvertisement[responses.size()]);
   }

   /**
    * Publishes the specified request(bytes) on the specifed topic
    * 
    * @param topicName -
    *           The topic to which the request must be published
    * @param request -
    *           The request to be published (in bytes)
    */
   public void send(String topicName, byte[] request) {
      producer.generateEventIdentifier(true);
      producer.setTemplateId(12345);
      producer.setDisableTimestamp(false);
      try {
         NBEvent nbEvent = producer.generateEvent(
                  TemplateProfileAndSynopsisTypes.STRING, topicName, request);
         producer.publishEvent(nbEvent);
      } catch (ServiceException e) {
         log.error("", e);
      }
   }

   public void close() {

      try {
         producer.close();
         consumer.unSubscribe(profile_TOPIC_DISCO_RESPONSE);

         Thread.sleep(2000);
         clientService.closeBrokerConnection();
         clientService.terminateServices();
      } catch (ServiceException e) {
         e.printStackTrace();
      } catch (InterruptedException e) {
         e.printStackTrace();
      }

   }

   public void onEvent(NBEvent nbEvent) {
      if (timedOut) return; // Request was timed out so drop this event

      String topic = (String) nbEvent.getContentSynopsis();

      if (topic.equals(Topics.TOPIC_DISCOVERY_RESPONSE)) {
         // What type of message was sent

         EncryptedMessagePayload emp = EncryptedMessagePayload
                  .createObjectFromBytes(nbEvent.getContentPayload());

         byte[] obj = emp.getDecryptedPayload(privateKey);

         if (obj == null) {
            log.error("Unable to decrypt Payload: ");
            return; // DISCARD, If error occurs
         }

         TopicDiscoveryResponse resp = TopicDiscoveryResponse
                  .createObjectFromBytes(obj);

         if (resp == null) {
            log.warn("{TOPIC_DISCOVERY_RESPONSE}Garbage EVENT recieved !");
            return;
         }

         if (!timedOut) {
            REMAINING_RESPONSES -= resp.getMatchingTopicCount();
            responses.addAll(resp.getMatchingTopicsCollection());
         }

         log.info(resp.getMatchingTopicCount() + " matching topics recieved !");
         // Unlock IFF REMAINING_RESPONSES == 0

         if (REMAINING_RESPONSES <= 0) {
            sync.unlock();
         }
      }
   }

   public static void main(String[] args) {
      if (args.length < 4) {
         System.out.println("USAGE: ");
         System.out.println("ARG_0: <broker host>");
         System.out.println("ARG_1: <broker port>");
         System.out.println("ARG_2: <connection protocol>");
         System.out.println("ARG_3: <benchmark_outputFile>");
         System.exit(0);
      }
      SystemInit.init();

      String host = "localhost";
      String port = "25000";
      String protocol = "niotcp";
      host = args[0];
      if (args.length > 1) port = args[1];
      if (args.length > 2) protocol = args[2];

      int clientEntityId = 55; // Integer.parseInt(args[0]);
      String alias = "c2";

      // MUST INITIALIZE THE CertificateManager
      CertificateManager certMan = new CertificateManager();

      String nbHome = null;
      try {
         nbHome = SystemInit.getInitData(SystemInit.SYS_NB_HOME);
      } catch (SystemNotInitializedException e2) {
         log.error("", e2);
      }

      // MUST INITIALIZE THE CertificateManager

      certMan.init(nbHome + "/keystore/NBSecurityTest.keys", null);

      Certificate tdnCert = CertificateUtil.getCertificate(certMan, alias);
      PrivateKey privkey = CertificateUtil.getPrivateKey(certMan, alias);

      // Should use broker discovery when its in place...
      // For now just use some broker and port...

      SignedTopicAdvertisement[] resps;

      TopicDiscoveryClient topicDiscoClient = new TopicDiscoveryClient(2000,
               nbHome + "/config/ServiceConfiguration.txt", tdnCert, privkey,
               host, port, protocol);

      if (BENCHMARK) {
         try {
            TopicDiscoveryClient.fout = new FileOutputStream(args[3]);
         } catch (FileNotFoundException e) {
            log.error("", e);
         }
      }

      // for (int i = 0; i < 120; i++) {
      // System.out.println("RUN: " + i);

      long start = topicDiscoClient.hrClock.getTimeMicroseconds();
      resps = topicDiscoClient.discover(Topics.MATCHING_STRING, "Topic1", null,
               1);

      long stop = topicDiscoClient.hrClock.getTimeMicroseconds();

      if (BENCHMARK) {
         try {
            fout.write(("TIME: " + (stop - start) + "\r\n").getBytes());
            System.out.println("TIME: " + (stop - start));
         } catch (IOException e1) {
            log.error("", e1);
         }
      }

      System.out.println("STRING Matched " + resps.length + " TOPICS !");

      resps = topicDiscoClient.discover(Topics.MATCHING_REGEX, "^To*", null, 1);
      System.out.println("REGEX Matched " + resps.length + " TOPICS !");

      // for (int i = 0; i < resps.length; i++) {
      // System.out.println("--Topic: " + i + "--\n" + resps[i].toString()
      // + "----\n");
      // }
      // }
      // resps = topicDiscoClient.discover("String", "NON_EXISTENT_TOPIC_1",
      // null,
      // 5);
      //
      // System.out.println("Matched " + resps.length + " TOPICS !");
      // for (int i = 0; i < resps.length; i++) {
      // System.out.println("--Topic: " + i + "--\n" + resps[i].toString()
      // + "----\n");
      // }

      System.exit(0);
   }

}